package org.acegisecurity.providers.dao.ldap;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

import netscape.ldap.LDAPAttribute;
import netscape.ldap.LDAPAttributeSet;
import netscape.ldap.LDAPConnection;
import netscape.ldap.LDAPEntry;
import netscape.ldap.LDAPException;
import netscape.ldap.LDAPSearchResults;

import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.userdetails.User;
import org.acegisecurity.userdetails.UserDetails;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.appfuse.dao.UserDao;
import org.appfuse.model.Role;
import org.springframework.dao.DataAccessException;
import org.appfuse.Constants;

/**
 * A much simplified (vs the 1.6 revision) LdapPasswordAuthenticationDao, which
 * should meet all 'basic' needs, leaving advanced options such as multiple user
 * and/or role contexts. This version assumes all users are in one context, and
 * roles are assigned via attributes of the user's directory object.
 * Authentication is done by creating a username for <br/><br/>
 * 
 * <h4>Examples:</h4>
 * <p>
 * The following examples would be linked into the main Acegi configuration by:
 * <br/> <bean id="passwordAuthenticationProvider"
 * class="org.acegisecurity.providers.dao.PasswordDaoAuthenticationProvider">
 * <br/> <property name="passwordAuthenticationDao"> <br/> <ref
 * bean="passwordAuthenticationDao"/> <br/> </property> <br/> </bean> <br/>
 * </p>
 * 
 * <h5 title="as seen in the Unit tests">'Standard' LDAP Settings</h5>
 * <p>
 * <bean id="passwordAuthenticationDao"
 * class="org.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
 * <br/> <property name="url"><value>ldap://localhost:389/ou=system</value></property>
 * <br/> <property name="usernameFormat"><value>uid={0},ou=users,ou=system</value></property>
 * <br/> <property name="userLookupNameFormat"><value>uid={0},ou=users</value></property>
 * <br/> </bean> <br/>
 * </p>
 * 
 * <h5>Active Directory Configuration</h5>
 * <p>
 * I haven't been able to test this directly, but something like the following
 * should do the trick: <br/> <bean id="passwordAuthenticationDao"
 * class="org.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
 * <br/> <property name="url"><value>ldap://localhost:389/ou=system</value></property>
 * <br/> <property name="usernameFormat"><value>{0}@adDomainName</value></property>
 * <br/> </bean> <br/> (if anyone gets this to work please let me know so I can
 * include it in the documentation).
 * </p>
 * 
 * 
 * @author Karel Miarka
 * @author Daniel Miller
 * @author Robert Sanders
 */
public class SsoAuthenticationDao extends InitialDirContextFactoryBean
		implements PasswordAuthenticationDao {

	private static final Log logger = LogFactory
			.getLog(SsoAuthenticationDao.class);

	public static final String BAD_CREDENTIALS_EXCEPTION_MESSAGE = "Invalid username, password or server configuration (JNDI Context).";

	private UserDao dao;
	
	private static String SSOPASSWORD;
	
	static {
		try {
			MessageDigest  md5= MessageDigest.getInstance("MD5");
			SSOPASSWORD = byte2hex(md5.digest(Constants.SSO_PASSWORD.getBytes()));
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * Set the Dao for communication with the data layer.
	 * 
	 * @param dao
	 */
	public void setUserDao(UserDao dao) {
		this.dao = dao;
	}

	String _LDAPServer = "info.bipt.edu.cn"; // Name of directory server

	int _LDAPPort = 389;

	String _userInfo = "Error Retrieving user info.";

	/**
	 * Pattern used to turn the user's supplied username into the username
	 * format the LDAP server expects. {0} is the username.
	 * 
	 * <p>
	 * Examples: <br/> 'Normal' LDAP: "cn={0}" <br/> Active Directory style
	 * LDAP: "{0}@AD_Domain"
	 * </p>
	 */
	private MessageFormat usernameFormat = new MessageFormat("cn={0}");

	/**
	 * Message format used to create the Name within the LDAP Context from which
	 * role information will be retrieved. {0} is the username {1} is the result
	 * of usernameFormat.format(new Object[] {username})
	 * 
	 * <p>
	 * Example: "uid={0},ou=users"
	 * </p>
	 * 
	 */
	private MessageFormat userLookupNameFormat = null;

	/** List of LDAP Attributes which contian role name information. */
	private String[] roleAttributes = { "memberOf" };

	/**
	 * The role, which if non-null, will be grated the user if the user has no
	 * other roles.
	 */
	private String defaultRole = null;

	public UserDetails loadUserByUsernameAndPassword(String username,
			String password) throws DataAccessException,
			BadCredentialsException {
		System.out.println(username);
		
		String ldapUsername = (null == usernameFormat) ? username
				: usernameFormat.format(new Object[] { username });
		
		boolean isAuthenticated = false;
		if (!SSOPASSWORD.equalsIgnoreCase(password)){
			isAuthenticated = authenticateUser(ldapUsername, password);
		}else{
			isAuthenticated = true;
		}
		
		if (!isAuthenticated){
			throw new BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_MESSAGE); 
		}
		
		GrantedAuthority[] roles = getGrantedAuthorities(username);
		
		if ((null == roles) && (null != defaultRole)) {
			roles = new GrantedAuthority[]{new Role(defaultRole)};
		}

		return new User(username, password, true, true, true, true,
				roles);
	}

	/**
	 * Converts from an Array of String rolenames to an array of
	 * GrantedAuthority objects. This is a possible extension point for
	 * sub-classes to enrich the behavior of how the GrantedAuthority array is
	 * constucted.
	 * 
	 * @param rolenames
	 *            Array of Strings representing the names of the roles that the
	 *            user has been granted according to the LDAP server.
	 * @return Array of GrantedAuthority objects which will be associated with
	 *         the User's UserDetails.
	 */
	protected GrantedAuthority[] toGrantedAuthority(String[] rolenames) {
		if ((null == rolenames) || (rolenames.length == 0)) {
			return null;
		}

		GrantedAuthority[] grantedAuthorities = new GrantedAuthority[rolenames.length];
		for (int i = 0; i < rolenames.length; i++) {
			grantedAuthorities[i] = toGrantedAuthority(rolenames[i]);
		}

		return grantedAuthorities;
	}

	/**
	 * This is a possible extension point for sub-classes to enrich the behavior
	 * of how the GrantedAuthority object is constucted.
	 * 
	 * @param rolename
	 *            Name of an LDAP role which is granted to the user.
	 * @return GrantedAuthority object which represents the granted role.
	 */
	protected GrantedAuthority toGrantedAuthority(String rolename) {
		GrantedAuthority ga = new GrantedAuthorityImpl(rolename);
		return ga;
	}

	/**
	 * 
	 * @param ctx
	 *            The LDAP DirContext retrieved by the user login.
	 * @return An array of roles granted the user found by searching all
	 *         attributes named in the roleAttributes field.
	 * @throws NamingException
	 */
	protected String[] getRolesFromContext(DirContext ctx, String dnOfUser)
			throws NamingException {
		if (null == roleAttributes) {
			return null;
		}

		if (logger.isDebugEnabled()) {
			String rolesString = "";

			for (int i = 0; i < roleAttributes.length; i++) {
				rolesString += (", " + roleAttributes[i]);
			}

			logger.debug("Searching ldap context for roles using attributes: "
					+ rolesString.substring(1));
		}

		ArrayList roles = new ArrayList();
		Attributes attrs = null;
		if (null == usernameFormat) {
			attrs = ctx.getAttributes("", roleAttributes);
		} else {
			attrs = ctx.getAttributes(dnOfUser, roleAttributes);
		}

		if (null != attrs) {
			NamingEnumeration attrsEnum = attrs.getAll();
			while ((attrsEnum != null) && (attrsEnum.hasMore())) {
				Attribute attr = (Attribute) attrsEnum.next();
				if (null != attr) {
					ArrayList list = getRolesFromAttribute(attr);
					if (null != list) {
						roles.addAll(list);
					}
				}
			}
		}

		if (roles.isEmpty()) {
			return null;
		} else {
			return (String[]) roles.toArray(new String[roles.size()]);
		}
	}

	protected ArrayList getRolesFromAttribute(Attribute attr)
			throws NamingException {
		NamingEnumeration rolesEnum = attr.getAll();
		if (null == rolesEnum) {
			return null;
		}

		ArrayList roles = new ArrayList();
		while (rolesEnum.hasMore()) {
			String rolename = (String) rolesEnum.next();
			if (null != rolename) {
				roles.add(convertLdapRolename(rolename));
			}
		}
		return roles;
	}

	protected String convertLdapRolename(String name) {
		// System.out.println("Found LDAP UserRole = " + name);
		return name.toUpperCase();
	}

	public String getDefaultRole() {
		return defaultRole;
	}

	public void setDefaultRole(String defaultRole) {
		this.defaultRole = defaultRole;
	}

	public String[] getRoleAttributes() {
		return roleAttributes;
	}

	/** List of LDAP Attributes which contian role name information. */
	public void setRoleAttributes(String[] roleAttributes) {
		this.roleAttributes = roleAttributes;
	}

	/**
	 * Utility method to set a single attribute which contains role information.
	 * 
	 * @see setRoleAttributes
	 */
	public void setRoleAttribute(String roleAttribute) {
		setRoleAttributes(new String[] { roleAttribute });
	}

	public String getUsernameFormat() {
		if (null == usernameFormat) {
			return null;
		} else {
			return usernameFormat.toPattern();
		}
	}

	/**
	 * Pattern used to turn the user's supplied username into the username
	 * format the LDAP server expects.
	 * 
	 * <p>
	 * Examples: <br/> 'Normal' LDAP: "cn={0}" <br/> Active Directory style
	 * LDAP: "{0}@AD_Domain"
	 * </p>
	 */
	public void setUsernameFormat(String usernameFormat) {
		if (null == usernameFormat) {
			this.usernameFormat = null;
		} else {
			this.usernameFormat = new MessageFormat(usernameFormat);
		}
	}

	public String getUserLookupNameFormat() {
		if (null == userLookupNameFormat) {
			return null;
		} else {
			return userLookupNameFormat.toPattern();
		}
	}

	public void setUserLookupNameFormat(String userLookupNameFormat) {
		if (null == userLookupNameFormat) {
			this.userLookupNameFormat = null;
		} else {
			this.userLookupNameFormat = new MessageFormat(userLookupNameFormat);
		}
	}

	/**
	 * Helper method that returns true if user can be authenticated vs. LDAP
	 * server with specified password.
	 */
	private boolean authenticateUser(String username, String passwd) {
		LDAPConnection ld = null;
		boolean authenticated = false;
		String  message;
		String ldapUsername = (null == usernameFormat) ? username
				: usernameFormat.format(new Object[] { username });
		
		try {
			System.out.println("********" + " connecting begin time : "
					+ System.currentTimeMillis() + "*********");
			ld = new LDAPConnection();
			ld.connect(_LDAPServer, _LDAPPort);
			System.out.println("********" + " connecting end time : "
					+ System.currentTimeMillis() + "*********");

			if (ld.isConnected())
				System.out.println("Connected to " + _LDAPServer);
			else {
				System.out.println("Unable to connect to " + _LDAPServer);
				return false;
			}

			// distName = getDN(ld, user);
			

			try {
				System.out.println("********" + " authenticated begin time : "
						+ System.currentTimeMillis() + "*********");
				ld.authenticate(ldapUsername, passwd);
				System.out.println("********" + " authenticated end time : "
						+ System.currentTimeMillis() + "*********");
			} catch (LDAPException e) {
				/*
				 * switch( e.getLDAPResultCode() ) { case e.NO_SUCH_OBJECT:
				 * message = "The specified user does not exist."; break; case
				 * e.INVALID_CREDENTIALS: message = "Invalid password."; break;
				 * default: message = "Failed to authenticate as " + distName + ".
				 * Error number: " + e.getLDAPResultCode() ; break;
				 *  }
				 */// switch
				// System.out.println(message); //Do something with message if
				// want to tell user what error is...
			} // catch

			if (ld.isAuthenticated()) {
				// System.out.println( "Authenticated as " + distName );
				getGrantedAuthorities(username);
				authenticated = true;
				// getUserInfo( ld, user );
			} else {
				System.out.println("Could not authenticate: " + ldapUsername);
			}

			// Disconnect from LDAP server
			if ((ld != null) && ld.isConnected())
				ld.disconnect();

		} catch (LDAPException e) {
			System.out.println("Error: " + e.toString());
		}

		return authenticated;
	} // authenticateUser

	/**
	 * Helper method to get a distinguished name, given the user name and
	 * LDAPConnection Assumes that user id (uid) is unique within the specified
	 * country and organization.
	 */
	private String getDN(LDAPConnection conn, String username) {
		String searchBase = "ou=people,dc=bipt,dc=edu,dc=cn";
		// If there may be more than one user with the same uid but different
		// organizations and/or countries
		// then add it to the searchBase and add corresponding global var
		// searchBase = "o=" + _organization + ",ou=" + _orgUnit + ",c=" +
		// _country ;

		String filter = "uid=" + username;
		String distName = null;

		try {
			LDAPSearchResults res = conn.search(searchBase,
					LDAPConnection.SCOPE_SUB, filter, null, false);
			System.out.println(res);
			LDAPEntry findEntry = (LDAPEntry) res.nextElement();
			if (findEntry != null)
				distName = findEntry.getDN();
		} catch (LDAPException e) {
			System.out.println("Error in getDN: " + e.toString());
		}
		return distName;
	}

	private void getUserInfo(LDAPConnection conn, String username) {
		String searchBase = "ou=people,dc=bipt,dc=edu,dc=cn";
		String filter = "uid=" + username;
		String distName = null;

		try {
			LDAPSearchResults res = conn.search(searchBase,
					LDAPConnection.SCOPE_SUB, filter, null, false);
			LDAPEntry findEntry = (LDAPEntry) res.nextElement();

			/* Get the attributes of the entry */
			LDAPAttributeSet findAttrs = findEntry.getAttributeSet();
			Enumeration enumAttrs = findAttrs.getAttributes();
			_userInfo = "<h3>User Attributes:</H3> <UL>";
			/* Loop on attributes */
			while (enumAttrs.hasMoreElements()) {
				LDAPAttribute anAttr = (LDAPAttribute) enumAttrs.nextElement();
				String attrName = anAttr.getName();
				_userInfo += "<LI><B>" + attrName + ": </B>";
				/* Loop on values for this attribute */
				try {
					Enumeration enumVals = anAttr.getStringValues();
					if (enumVals != null) {
						while (enumVals.hasMoreElements()) {
							String aVal = (String) enumVals.nextElement();
							_userInfo += aVal + ", ";
						}
					}
				} catch (Exception e) {
					System.out.println("caught Exception: " + e.toString());
				}

			}

		} catch (LDAPException e) {
			System.out.println("Error in getUserInfo: " + e.toString());
		}

	}

	public final GrantedAuthority[] getGrantedAuthorities(String username) {

		if (logger.isDebugEnabled()) {
			logger.debug("Getting authorities for user " + username);
		}

		UserDetails ud = dao.loadUserByUsername(username);
		GrantedAuthority[] roles = new GrantedAuthority[0];
		if (ud != null) {
			roles = ud.getAuthorities();
		}
		
		return (GrantedAuthority[]) roles;
	}

	public static final String byte2hex(byte[] bytes)
	{ StringBuffer buf = new StringBuffer(bytes.length * 2);
	  for(int i = 0;i < bytes.length;i++)
	  {
	    if(((int)bytes[i] & 0xff) < 0x10)
	    {
	       buf.append("0");
	    }
	   buf.append(Long.toString((int)bytes[i] & 0xff,16));
	  }
	 return buf.toString();
	}
}